Volt routing problem
Hi!
I've deployed my SaaS application built with Wave, but I'm running into a routing error when using Volt.
The URL in question is:
http://mywebsite.com/projects/9ed0a5f0-7d9b-4609-b166-94490a4b9a98
The error I'm getting is:
Cannot assign string to property Livewire\Volt\Component@anonymous::$project of type App\Models\Project
@volt('project')
I'm using UUIDs for the id field, and this works perfectly fine in my local environment. The model uses the HasUuids trait, and the migration defines the primary key like this:
$table->uuid('id')->primary();
What could be causing this issue in production?
Hey!
I just tested this and it seems to be working for me:
Would you mind sharing your exact volt component? Also what database are you using in production?
Hi Bobby,
Thanks for your response!
On production i am using mysql instead of sqlite.
<?php
use function Laravel\Folio\{middleware, name};
use Livewire\Volt\Component;
use App\Models\Project;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Infolists\Components\Split;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Components\Tabs;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use App\Infolists\Components\ProjectProgressBar;
use Filament\Infolists\Components\Fieldset;
use Filament\Forms\Components\DatePicker;
use Filament\Infolists\Components\Actions\Action;
use Filament\Forms\Components\Radio;
use App\Models\Token;
use App\Models\DealToken;
use Filament\Notifications\Notification;
use Filament\Actions\EditAction;
use Filament\Actions\Contracts\HasActions;
use Filament\Actions\Concerns\InteractsWithActions;
use App\Actions\CalculateDealUnlock;
use Filament\Actions\DeleteAction;
use App\Livewire\UnlockTable;
use Livewire\Attributes\On;
use App\Livewire\DealsTable;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
middleware('auth');
name('project');
new class extends Component implements HasForms, HasInfolists, HasActions {
use InteractsWithInfolists;
use InteractsWithForms;
use InteractsWithActions;
use AuthorizesRequests;
public Project $project;
public bool $isMarketDataLinked = false;
public function mount(Project $project)
{
$this->authorize('view', $project);
$this->project = $project;
if ($this->project->token) {
$this->isMarketDataLinked = true;
}
}
public function editAction(): EditAction
{
return EditAction::make()
->hiddenLabel()
->button()
->icon('heroicon-m-pencil-square')
->record($this->project)
->mutateFormDataUsing(function ($data) {
$data['ticker'] = Str::upper($data['ticker']);
$data['name'] = Str::ucfirst(Str::lower($data['name']));
return $data;
})
->form(Project::getForm());
}
public function deleteAction(): DeleteAction
{
return DeleteAction::make()
->record($this->project)
->hiddenLabel()
->button()
->color('danger')
->icon('heroicon-m-trash')
->successRedirectUrl(route('projects'))
->requiresConfirmation();
}
#[on('refreshParent')]
public function productInfolist(Infolist $infolist): Infolist
{
return $infolist
->record($this->project)
->schema([
Split::make([
Section::make('project_information')
->extraAttributes(['class' => 'h-full'])
->heading('Project information')
->schema([
ImageEntry::make('image')
->label(false)
->alignCenter()
->circular()
->size(150),
TextEntry::make('name')
->label(false)
->alignCenter()
->size(TextEntry\TextEntrySize::Large),
ProjectProgressBar::make('unlock_percentage')
->label(false)
->default(function () {
$totalTokens = $this->project->unlocks->sum('token_amount');
$unlockedTokens = $this->project->unlocks->where('unlock_date', '<',
today())->sum('token_amount');
return $totalTokens > 0
? round(($unlockedTokens / $totalTokens) * 100, 2)
: 0;
}),
]),
Section::make('my_contribution')
->extraAttributes(['class' => 'h-full'])
->heading('My contribution')
->schema([
\Filament\Infolists\Components\Livewire::make(\App\Livewire\ContributionTable::class,
['project' => $this->project])->key('contribution-table')
]),
Section::make('market_information')
->extraAttributes(['class' => 'h-full'])
->heading('Current Market Data')
->headerActions([
Action::make('link')
->color('warning')
->icon('heroicon-m-link')
->fillForm(fn(Project $record): array => [
'token' => $record->token->id ?? null,
])
->form([
Radio::make('token')
->required()
->options(function ($record) {
return Token::where('name', $record->name)->orWhere('ticker',
$record->ticker)->pluck('name', 'id')->toArray();
})
])
->action(function (Project $record, array $data, array $arguments) {
if ($arguments['unlink'] ?? false) {
DealToken::where('project_id', $this->project->id)->firstorFail()->delete();
return Notification::make()
->title('Token unlinked!')
->success()
->send();
}
DealToken::updateOrCreate(
['project_id' => $record->id],
['token_id' => $data['token']]
);
return Notification::make()
->title('Token saved!')
->success()
->send();
})
->after(function () {
$this->dispatch('refresh-data')->to(UnlockTable::class);
})
->modalHeading('Choose token')
->extraModalFooterActions(fn(Action $action): array => [
$action->makeModalSubmitAction('unlink', arguments: ['unlink' => true]),
]),
])
->schema([
Fieldset::make('market_data')
->label(false)
->schema([
TextEntry::make('token.ticker')
->label('Ticker')
->placeholder('-'),
TextEntry::make('token.price')
->formatStateUsing(function($state){
if((float)$state >= 0.01){
return round($state,3);
} else {
return $state;
}
})
->label('Price')
->prefix('$')
->placeholder('-'),
TextEntry::make('project_roi')
->default(function(){
return round((($this->project->token->price - $this->project->averageBuyPrice()) / $this->project->averageBuyPrice()) * 100,2);
})
->prefix(function ($state) {
if ($state > 0) {
return '+';
}
})
->color(function ($state) {
if ($state < 0) {
return 'danger';
} else {
return 'success';
}
})
->suffix('%')
->label('Average price roi')
->placeholder('-'),
TextEntry::make('token.ticker')
->hidden(function () {
return $this->isMarketDataLinked;
})
->label('Ticker')
->placeholder('-'),
TextEntry::make('token.day_change')
->formatStateUsing(function ($state) {
return round($state, 2);
})
->prefix(function ($state) {
if ($state > 0) {
return '+';
}
})
->color(function ($state) {
if ($state < 0) {
return 'danger';
} else {
return 'success';
}
})
->suffix('%')
->label('24H change')
->placeholder('-'),
TextEntry::make('token.atl')
->label('All time high')
->money()
->placeholder('-'),
TextEntry::make('token.atl')
->label('All time low')
->formatStateUsing(function ($state) {
return round($state, 2);
})
->prefix(function ($state) {
if ($state > 0) {
return '+';
}
})
->color(function ($state) {
if ($state < 0) {
return 'danger';
} else {
return 'success';
}
})
->suffix('%')
->placeholder('-'),
])->columns(3),
Fieldset::make('tge_date')
->label(false)
->schema([
TextEntry::make('tge_date')
->tooltip('Token generation event')
->badge()
->default('')
->placeholder('-')
->suffixAction(
Action::make('setTgeDate')
->label(function ($record) {
if ($record->tge_date) {
return 'Edit';
} else {
return 'Add';
}
})
->button()
->fillForm(fn(Project $record): array => [
'tge_date' => $record->tge_date,
])
->form([
DatePicker::make('tge_date')
->required()
])
->action(function (Project $record, $data) {
$record->tge_date = $data['tge_date'];
$record->save();
if ($record->wasChanged('tge_date')) {
$this->regenerateUnlock();
}
return Notification::make()
->title('Tge date saved!')
->success()
->send();
})
->modalHeading('Set Tge date')
->requiresConfirmation()
->after(function () {
$this->dispatch('refresh-data')->to(UnlockTable::class);
$this->dispatch('refresh-data')->to(DealsTable::class);
})
)->columnSpanFull()
])
])
])->columns(3)->extraAttributes(['class' => 'items-stretch']),
Split::make([
Section::make('deals')
->heading(false)
->schema([
Tabs::make('Tabs')
->tabs([
Tabs\Tab::make('Deals')
->badge(function (Project $record) {
return $record->countDeals();
})
->icon('phosphor-handshake-fill')
->schema([
\Filament\Infolists\Components\Livewire::make(DealsTable::class,
['project' => $this->project])->key('deals-table')
]),
Tabs\Tab::make('Sell Orders')
->badge(function (Project $record) {
return $record->sellOrders()->count();
})
->icon('heroicon-m-currency-dollar')
->schema([
\Filament\Infolists\Components\Livewire::make(\App\Livewire\SellOrdersTable::class,
['project' => $this->project])->key('sell-orders-table')
]),
Tabs\Tab::make('Profit Overview')
->icon('heroicon-m-chart-bar')
->schema([
// \Filament\Infolists\Components\Livewire::make(\App\Livewire\ProjectProfitOverview::class,
// ['project' => $this->project])->key('profit-overview-widget')
]),
])->contained(false)
])
->columnSpan(2),
Section::make('unlock_schedule')
->heading('$'.$this->project->ticker.' unlock schedule')
// ->headerActions([
// \Filament\Infolists\Components\Actions\Action::make('manage_unlocks')
// ->label('Manage')
// ])
->schema([
\Filament\Infolists\Components\Livewire::make(UnlockTable::class,
['project' => $this->project])->key('unlock-table')
])
->grow(false)
->columnSpan(1),
]),
]);
}
public function regenerateUnlock(): void
{
$createUnlocks = new CalculateDealUnlock();
foreach ($this->project->deals as $deal) {
$createUnlocks->handle($deal);
\App\Actions\CheckDealStatus::handle($deal);
}
}
}
?>
<x-layouts.app>
@volt('project')
<div>
<div class="px-2 flex justify-between items-center ">
<nav class="flex" aria-label="Breadcrumb">
<ol role="list" class="flex items-center space-x-4">
<li>
<div>
<a href="/dashboard" class="text-gray-400 hover:text-gray-500">
<svg class="h-5 w-5 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M9.293 2.293a1 1 0 011.414 0l7 7A1 1 0 0117 11h-1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3a1 1 0 00-1-1H9a1 1 0 00-1 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-6H3a1 1 0 01-.707-1.707l7-7z"
clip-rule="evenodd"/>
</svg>
<span class="sr-only">Home</span>
</a>
</div>
</li>
<li>
<div class="flex items-center">
<svg class="h-5 w-5 flex-shrink-0 text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"/>
</svg>
<a href="/projects"
class="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700">Projects</a>
</div>
</li>
<li>
<div class="flex items-center">
<svg class="h-5 w-5 flex-shrink-0 text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"/>
</svg>
<a href="#" class="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
aria-current="page">{{ $this->project->name }}</a>
</div>
</li>
</ol>
</nav>
<div class="ml-auto flex items-center space-x-4">
{{ $this->editAction() }}
{{ $this->deleteAction() }}
</div>
</div>
<div class="mt-5 pb-10">
{{ $this->productInfolist }}
<x-filament-actions::modals/>
</div>
</div>
@endvolt
</x-layouts.app>
Switched to a livewire component, but same error
Maybe a folio error?
Hey there! 👋
It looks like Folio is passing a string (maybe the project ID or slug?) to your Livewire component, but your $project
property is typed as a Project
model, which is why you're seeing that type error.
If I remember correctly, Folio doesn't do route model binding out of the box (at least not like standard Laravel routes), you might need to either:
- Load the model manually in your component, like so:
public string $project;
public Project $loadedProject;
public function mount()
{
$this->loadedProject = Project::findOrFail($this->project);
}
- Or resolve the model in your Blade file before passing it in:
<?php
use App\Models\Project;
$projectModel = Project::findOrFail($project);
?>
@livewire('view-project', ['project' => $projectModel])
Not 100% sure without seeing how you're passing the parameter in, but this should be pretty close. If you're using a slug instead of an ID, you'd just want to swap in where('slug', $this->project)->firstOrFail()
instead.
thanks! after my vacation i will test it and let you know.
I cant seem to get in work on production. Local everything is working fine. So route model binding also works. Switched back to a Volt component, like in my first post.
I am using an Uuid as parameter:
http://mywebsite.com/projects/9ed0a5f0-7d9b-4609-b166-94490a4b9a98
Attached the full error page.
Can someone help me please?
Hey!
I've not been able to reproduce the issue on my end.
I just tested this with the exact same setup you're using — including:
-
resources/views/projects/[project].blade.php
using an inline Volt component - Route model binding via
public Project $project
andmount(Project $project)
- UUIDs via
HasUuids
in the model - MySQL with
uuid('id')->primary()
in the migration
It all works great on MySQL in production-like conditions.
Here's the minimal setup I used:
resources/views/projects/[project].blade.php
<?php
use App\Models\Project;
use Livewire\Volt\Component;
use function Laravel\Folio\{middleware, name};
middleware('auth');
name('project');
new class extends Component {
public Project $project;
public function mount(Project $project)
{
$this->project = $project;
}
}
?>
<x-layouts.app>
@volt('project')
<div class="p-6">
<h2 class="text-2xl font-bold">Project Details</h2>
<p class="mt-2 text-gray-700">Name: {{ $this->project->name }}</p>
<p class="text-sm text-gray-500">ID: {{ $this->project->id }}</p>
<a href="/projects" class="mt-4 inline-block text-blue-600 underline">← Back to Projects</a>
</div>
@endvolt
</x-layouts.app>
Project
Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Project extends Model
{
use HasUuids;
protected $fillable = ['name'];
}
Migration
Schema::create('projects', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('name');
$table->timestamps();
});
If it's still not working for you in production, could you share a minimal reproducible version that I can test with?
Without being able to reproduce the issue, it is difficult to say what might be going wrong.